Skip to content

Better Presets - #61

Merged
tg666 merged 5 commits into
masterfrom
feature/better-presets
Feb 23, 2026
Merged

Better Presets#61
tg666 merged 5 commits into
masterfrom
feature/better-presets

Conversation

@tg666

@tg666 tg666 commented Feb 23, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • srcSet can be called without a descriptor, simplifying responsive image usage.
    • Signature generation/verification can skip known modifier combinations (configurable), avoiding tokens for common presets.
  • Documentation

    • Preset examples updated: per-preset modifier properties moved into a dedicated modifiers block.
  • Refactor

    • Preset handling now uses structured preset objects and collections for clearer, typed configuration.

- presets are now connected to srcSet descriptors
- providing a descriptor in methods `FileInfoInterface::srcSet()` and `LinkGeneratorInterface::srcSet()` is now optional, the descriptor is automatically resolved from used preset
- added configuration option `disable_signature_on_known_modifiers`
- known modifier paths are now resolved during DI container build time
@coderabbitai

coderabbitai Bot commented Feb 23, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dcea5ba and e8ed73c.

📒 Files selected for processing (5)
  • src/Modifier/Codec/Codec.php
  • src/Responsive/Descriptor/DescriptorInterface.php
  • src/Responsive/Descriptor/WDescriptor.php
  • src/Responsive/Descriptor/XDescriptor.php
  • tests/Modifier/Codec/CodecTest.phpt

📝 Walkthrough

Walkthrough

This PR replaces Value-wrapper codec APIs with direct string/array methods, introduces PresetConfig and Preset value objects, makes descriptor parameters optional with lazy resolution, and adds KnownModifiers-driven conditional signature creation/verification.

Changes

Cohort / File(s) Summary
Configuration & DI
README.md, src/Bridge/Nette/DI/Config/PresetConfig.php, src/Bridge/Nette/DI/Config/StorageConfig.php, src/Bridge/Nette/DI/ImageStorageExtension.php, src/Config/Config.php
Adds PresetConfig, nests preset properties under modifiers, updates StorageConfig docblock to use PresetConfig, converts configured presets to Preset objects, builds KnownModifiers, and wires it into signature strategy; new DISABLE_SIGNATURE_ON_KNOWN_MODIFIERS config constant.
Codec core & API
src/Modifier/Codec/Codec.php, src/Modifier/Codec/CodecInterface.php, src/Modifier/Codec/PresetCodec.php, src/Modifier/Codec/RuntimeCachedCodec.php
Replaces encode/decode (ValueInterface) with modifiersToPath/pathToModifiers and expandModifiers; PresetCodec now depends on ConfigInterface and ModifierCollectionInterface; runtime cache unified and adapted to new API.
Removed codec value types
src/Modifier/Codec/Value/ValueInterface.php, src/Modifier/Codec/Value/Value.php, src/Modifier/Codec/Value/PresetValue.php
Removes Value wrapper interface and classes (no longer used with direct string/array codec API).
Preset objects & collections
src/Modifier/Preset/Preset.php, src/Modifier/Preset/PresetCollection.php, src/Modifier/Preset/PresetCollectionInterface.php
Introduces immutable Preset (modifiers, descriptor, defaultDescriptorValue); PresetCollection and its interface now accept/return Preset objects instead of arrays.
srcSet optional descriptor & link resolution
src/FileInfo.php, src/FileInfoInterface.php, src/ImageStorage.php, src/LinkGenerator/LinkGenerator.php, src/LinkGenerator/LinkGeneratorInterface.php
Makes descriptor parameter nullable/default-null across srcSet APIs; LinkGenerator::srcSet lazily resolves a descriptor when null (from preset/modifier strings); query-signature injection guarded to avoid empty tokens.
Modifier facade & factory
src/Modifier/Facade/ModifierFacade.php, src/Modifier/Facade/ModifierFacadeFactory.php, src/Modifier/Facade/ModifierFacadeInterface.php
Facade uses expandModifiers for string presets, exposes getPresetCollection(), factory switches to named args for nested construction; interface updated to accept Preset objects.
Descriptors: validation & expansion
src/Responsive/Descriptor/DescriptorInterface.php, src/Responsive/Descriptor/WDescriptor.php, src/Responsive/Descriptor/XDescriptor.php
Adds validateModifierValue, expandModifier, and iterateModifiers to descriptor API; WDescriptor and XDescriptor implement validation, expansion and iteration for width and pixel-density descriptors.
Security: known modifiers & signatures
src/Security/KnownModifiers.php, src/Security/SignatureStrategy.php, src/Security/SignatureStrategyInterface.php
Adds KnownModifiers lookup; SignatureStrategy now depends on it, createToken may return null for known paths, verifyToken added, and isKnown logic implemented; interface adjusted to nullable token return.
Supporting updates
src/PathInfo.php, src/PathInfoInterface.php, src/ImageServer/LocalImageServer.php, src/Responsive/Descriptor/ArgsFacade.php, src/Responsive/SrcSetGenerator.php
Call sites switched to pathToModifiers/modifiersToPath/expandModifiers, simplified signature handling and cache lookup, minor doc/cleanup changes.
Modifier internals
src/Modifier/AbstractModifier.php, src/Modifier/Codec/*
Marks AbstractModifier constructor final; codec internals reworked to new API and error semantics.
Tests & configs
tests/..., tests/Bridge/Nette/DI/config/...neon, README.md
Extensive test updates: replace array presets with Preset objects, codec API renames in tests, DI/config NEON preset changes (modifiers nested), KnownModifiers tests, and assertion adjustments.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant LinkGen as LinkGenerator
    participant Presets as PresetCollection
    participant Descriptor as Descriptor
    participant Signature as SignatureStrategy
    participant Known as KnownModifiers

    Client->>LinkGen: srcSet(info, descriptor?=null)
    alt descriptor is null and modifiers is string
        LinkGen->>Presets: get(presetAlias)
        Presets-->>LinkGen: Preset(modifiers, descriptor?, default)
        alt preset has descriptor and value provided
            LinkGen->>Descriptor: validateModifierValue(value, default)
            Descriptor-->>LinkGen: validated value
            LinkGen->>Descriptor: expandModifier(modCollection, value)
            Descriptor-->>LinkGen: modifier map
        else
            LinkGen->>Presets: use preset.modifiers
        end
    end
    LinkGen->>Signature: createToken(path)
    Signature->>Known: isKnown(path)
    alt known
        Known-->>Signature: true
        Signature-->>LinkGen: null
    else
        Known-->>Signature: false
        Signature-->>LinkGen: token
    end
    LinkGen-->>Client: SrcSet (with/without token)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • Better Presets #61 — similar "better presets" refactor that introduces Preset/PresetConfig and updates presets handling across codec, collection, and DI.
  • Absolute X Relative links #60 — touches the same srcSet/link APIs (FileInfo, ImageStorage, LinkGenerator) and may overlap on nullable descriptor changes.

Suggested labels

enhancement

Poem

🐰 I nibbled through codecs with nimble paws,
Swapped wrappers for strings without a pause.
Presets bundled neatly, descriptors now light,
Known hops skip signatures — what a bright sight!
A twitch, a patch, and then a joyful byte.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Better Presets' is vague and generic, using a non-specific term ('Better') that doesn't convey meaningful information about the actual changes in the changeset. Consider a more descriptive title that highlights the primary structural change, such as 'Refactor preset configuration to use structured PresetConfig and modifiers block' or 'Restructure presets to support optional descriptors and modifiers isolation.'
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/better-presets

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
tests/ImageServer/LocalImageServerTest.phpt (1)

366-366: ⚠️ Potential issue | 🟡 Minor

Typo in test method name: OnlSourceOnlySource.

✏️ Proposed fix
-    public function testImageResponseShouldBeReturnedIfOnlSourceFileExists(): void
+    public function testImageResponseShouldBeReturnedIfOnlySourceFileExists(): void
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/ImageServer/LocalImageServerTest.phpt` at line 366, Rename the test
method testImageResponseShouldBeReturnedIfOnlSourceFileExists to
testImageResponseShouldBeReturnedIfOnlySourceFileExists to fix the typo; update
the method declaration and any references (e.g., in test suites or annotations)
that call or reference testImageResponseShouldBeReturnedIfOnlSourceFileExists so
they point to the corrected method name.
tests/Responsive/Descriptor/ArgsFacadeTest.phpt (1)

11-11: ⚠️ Potential issue | 🟡 Minor

Unused import flagged by CI — PresetValue is no longer used.

The pipeline failure confirms no_unused_imports violations. PresetValue is imported but not referenced after the migration to expandModifiers.

Proposed fix
-use SixtyEightPublishers\ImageStorage\Modifier\Codec\Value\PresetValue;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Responsive/Descriptor/ArgsFacadeTest.phpt` at line 11, Remove the
now-unused import PresetValue from the test file (the import statement
referencing SixtyEightPublishers\ImageStorage\Modifier\Codec\Value\PresetValue)
since it’s no longer referenced after migrating to expandModifiers; locate the
import in ArgsFacadeTest (tests/Responsive/Descriptor/ArgsFacadeTest.phpt) and
delete that use line (or consolidate imports) so the no_unused_imports CI
violation is resolved.
tests/Modifier/Facade/ModifierFacadeTest.phpt (1)

16-16: ⚠️ Potential issue | 🟡 Minor

Unused import: PresetValue is no longer referenced in this test file.

The pipeline's PHP CS Fixer failure confirms no_unused_imports violations. This import is a leftover from the old decode(PresetValue) flow that was replaced by expandModifiers.

🧹 Proposed fix
-use SixtyEightPublishers\ImageStorage\Modifier\Codec\Value\PresetValue;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Modifier/Facade/ModifierFacadeTest.phpt` at line 16, Remove the unused
import PresetValue from the top of the test (it is no longer referenced in
ModifierFacadeTest after replacing decode(PresetValue) with expandModifiers);
locate the use statement "use
SixtyEightPublishers\ImageStorage\Modifier\Codec\Value\PresetValue;" in
ModifierFacadeTest and delete that line so PHP CS Fixer no_unused_imports
violations are resolved.
🧹 Nitpick comments (12)
src/Responsive/Descriptor/WDescriptor.php (1)

87-108: Duplicated validation logic with validateModifierValue.

Lines 95–107 repeat the same numeric-check-and-in_array logic found in validateModifierValue (lines 74–84). Consider delegating to validateModifierValue (or extracting a shared private helper) to keep validation consistent in one place.

Proposed refactor
 public function expandModifier(
     ModifierCollectionInterface $modifierCollection,
     mixed $value,
 ): array {
     $wAlias = $modifierCollection
         ->getByName(Width::class)
         ->getAlias();

-    if (is_numeric($value) && in_array((int) $value, $this->widths, true)) {
-        return [
-            $wAlias => (int) $value,
-        ];
-    }
-
-    throw new InvalidArgumentException(
-        message: sprintf(
-            'Invalid preset value "%s" passed for descriptor %s',
-            var_export($value, true),
-            $this,
-        ),
-    );
+    return [
+        $wAlias => $this->validateModifierValue($value, null),
+    ];
 }

Note: validateModifierValue($value, null) with null default works because $value here is expected to be numeric already (not true). If true can be passed in this context, adjust the second argument accordingly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Responsive/Descriptor/WDescriptor.php` around lines 87 - 108,
expandModifier duplicates the numeric + in_array validation already implemented
in validateModifierValue; remove the duplicated logic and delegate to that
method (or call a new shared private helper) to ensure single-source validation.
Specifically, inside WDescriptor::expandModifier call
$this->validateModifierValue($value, null) (or the extracted helper) and use its
result to build and return the array with the width alias (obtained via
getByName(Width::class)->getAlias()); if validation fails let
validateModifierValue throw the same InvalidArgumentException so expandModifier
no longer repeats the numeric/in_array checks.
tests/Modifier/Preset/PresetCollectionTest.phpt (1)

17-36: Tests properly updated for Preset objects.

The test covers add/has/get and the error case. Consider adding assertions for $descriptor and $defaultDescriptorValue properties on the retrieved Preset to ensure the full object round-trips correctly through the collection.

Example additional assertions
     Assert::same(['w' => 15], $collection->get('a')->modifiers);
+    Assert::null($collection->get('a')->descriptor);
+    Assert::null($collection->get('a')->defaultDescriptorValue);
     Assert::same(['w' => 15, 'pd' => 2.0], $collection->get('b')->modifiers);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Modifier/Preset/PresetCollectionTest.phpt` around lines 17 - 36, Add
assertions in testPresetsShouldBeAdded to verify that Preset objects are
round-tripped with their descriptor and defaultDescriptorValue intact: after
retrieving each Preset via PresetCollection::get('a') and ::get('b'), assert the
Preset->descriptor equals the expected descriptor (null in the current diff) and
Preset->defaultDescriptorValue equals the expected default (null in the current
diff) so both properties are explicitly checked along with modifiers; update
expected values if different from null.
src/Modifier/Codec/Codec.php (1)

40-47: Duplicate default-assignment logic between modifiersToPath and pathToModifiers.

The assigner/separator default logic (fallback to ':' / ',' when empty) is duplicated in both methods (lines 40–44 here and lines 79–80 in pathToModifiers). Consider extracting a small private helper to keep this in one place.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Modifier/Codec/Codec.php` around lines 40 - 47, The default
assigner/separator fallback logic is duplicated in modifiersToPath and
pathToModifiers; extract a private helper method (e.g., private function
normalizeModifierDelimiters(array $config): array) that reads
$this->config[Config::MODIFIER_ASSIGNER] and Config::MODIFIER_SEPARATOR, applies
the empty() ? ':' / ',' defaults, asserts string types, and returns
['assigner'=>..., 'separator'=>...]; then replace the duplicated blocks in
modifiersToPath and pathToModifiers to call this helper and use the returned
assigner and separator variables.
src/Security/KnownModifiers.php (1)

7-19: LGTM — minor naming note on $list.

The implementation is correct: array<string, true> as a hash-set with O(1) isset lookup is the right approach. list is permitted as a property name since PHP 7.0, as reserved keywords are allowed for property, constant, and method names of classes, interfaces, and traits, so there's no technical issue here. The property name $list is a bit generic though — a more descriptive name like $modifierPaths would better convey its contents.

✏️ Proposed rename for clarity
-    /**
-     * `@param` array<string, true> $list
-     */
-    public function __construct(
-        public readonly array $list,
-    ) {}
+    /**
+     * `@param` array<string, true> $modifierPaths
+     */
+    public function __construct(
+        public readonly array $modifierPaths,
+    ) {}

     public function isKnown(string $modifiers): bool
     {
-        return isset($this->list[$modifiers]);
+        return isset($this->modifierPaths[$modifiers]);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Security/KnownModifiers.php` around lines 7 - 19, Rename the generic
property $list to a clearer name (e.g., $modifierPaths) in the KnownModifiers
class: update the constructor parameter and promoted property, the public
readonly property declaration, and all usages such as the isKnown(string
$modifiers) method to reference the new name ( KnownModifiers::$modifierPaths
and its constructor promotion ). Ensure the docblock stays accurate
(array<string, true>) and run tests or static analysis to catch any remaining
references to $list.
tests/Modifier/Codec/RuntimeCachedCodecTest.phpt (1)

47-75: Test method names are inverted in order.

testStringValueShouldBeDecodedAndCached2 (line 47) appears before testStringValueShouldBeDecodedAndCached (line 62). The 2 suffix conventionally implies a follow-up test, but here it comes first, which is confusing. Consider renaming them to reflect their actual purpose, e.g. testPathCanBeDecodedAndResultIsCached and testCachedResultIsReturnedWithoutCallingInnerCodecAgain.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Modifier/Codec/RuntimeCachedCodecTest.phpt` around lines 47 - 75, The
test method names are out of logical order and unclear: rename the two methods
(currently testStringValueShouldBeDecodedAndCached2 and
testStringValueShouldBeDecodedAndCached) to clearer, purpose-revealing names
such as testPathCanBeDecodedAndResultIsCached for the one asserting
decoding+cache behavior and
testCachedResultIsReturnedWithoutCallingInnerCodecAgain for the one asserting
the inner codec is only called once; update both method declarations (and any
references) accordingly so their names reflect intent and order.
README.md (1)

90-99: LGTM — minor indentation inconsistency in the example.

The new nested modifiers: structure and the w/defaultW top-level preset fields are clearly illustrated. One nit: my_preset's modifiers children use 2-space extra indentation (lines 93–94), while my_preset_2's use 4-space extra (line 97). Both are valid NEON, but aligning them aids readability.

📝 Proposed consistency fix
             presets:
                 my_preset:
                     modifiers:
-                      w: 150
-                      ar: '2x1.5'
+                        w: 150
+                        ar: '2x1.5'
                 my_preset_2:
                     modifiers:
                         ar: 1x2
                     w: [300, 600, 900]
                     defaultW: 600
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 90 - 99, The README example has inconsistent
indentation for the presets: align the nested modifiers children consistently
between my_preset and my_preset_2 (e.g., make the w and ar under my_preset use
the same 4-space extra indentation style as my_preset_2); update the modifiers
block under my_preset so the w and ar lines match the indentation of my_preset_2
(presets, my_preset, my_preset_2, modifiers, w, ar, defaultW).
src/Modifier/Codec/PresetCodec.php (1)

62-63: Implicit array initialization of $modifiers on line 63.

$modifiers is first used with $modifiers[] = $preset->modifiers without prior declaration. While this is valid PHP that auto-creates the array, an explicit initialization improves readability.

♻️ Optional: explicit initialization
         $preset = $this->presetCollection->get(presetAlias: $presetAlias);
-        $modifiers[] = $preset->modifiers;
+        $modifiers = [$preset->modifiers];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Modifier/Codec/PresetCodec.php` around lines 62 - 63, The code uses
$modifiers[] = $preset->modifiers without prior declaration; explicitly
initialize $modifiers as an empty array before its first use (e.g., add
$modifiers = [] before the loop or before the line that calls
$this->presetCollection->get) so the variable is declared clearly; update the
function in PresetCodec (where $presetAlias is retrieved via
$this->presetCollection->get) to declare $modifiers = [] prior to appending.
src/LinkGenerator/LinkGenerator.php (2)

49-51: getModifiers() called twice on line 49.

Consider caching the result in a local variable to avoid the redundant call and improve readability.

♻️ Optional: cache modifiers
-        if (null === $pathInfo->getModifiers() || [] === $pathInfo->getModifiers()) {
+        $modifiers = $pathInfo->getModifiers();
+        if (null === $modifiers || [] === $modifiers) {
             $pathInfo = $pathInfo->withModifiers(['original' => true]);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/LinkGenerator/LinkGenerator.php` around lines 49 - 51, The code calls
$pathInfo->getModifiers() twice; cache its result in a local variable (e.g.,
$modifiers = $pathInfo->getModifiers()) and use that variable for the null/empty
check and the subsequent logic so you only invoke getModifiers() once; if
$modifiers is null or an empty array call $pathInfo =
$pathInfo->withModifiers(['original' => true]) as before.

101-124: Duplicated assigner-fallback logic across LinkGenerator and PresetCodec.

Lines 107-108 mirror the same pattern in PresetCodec::doExpand (lines 55-56). If the fallback logic changes, both locations need updating.

Consider extracting the assigner resolution (with the empty-string fallback to ':') into a shared utility or Config method to keep it DRY. This is not urgent but worth tracking for a follow-up.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/LinkGenerator/LinkGenerator.php` around lines 101 - 124, The
assigner-fallback logic duplicated in LinkGenerator::resolveDescriptor (using
$this->config[Config::MODIFIER_ASSIGNER] with an empty-string fallback to ':')
should be extracted into a single utility method (e.g.,
Config::getModifierAssigner() or a small helper like
ModifierAssigner::resolve()) and then used from both
LinkGenerator::resolveDescriptor and PresetCodec::doExpand; update
resolveDescriptor to call the new method instead of repeating lines 107-108 and
remove the duplicate logic from PresetCodec so both locations rely on the shared
implementation.
src/Bridge/Nette/DI/ImageStorageExtension.php (1)

619-636: Variable $modifiers on line 628 shadows the outer $modifiers declared on line 584.

Inside the foreach loop, $modifiers = $preset->modifiers overwrites the reference to the modifier instances array built on lines 584–593. While no code after the loop uses the outer $modifiers, this shadowing is a maintenance hazard.

♻️ Rename the inner variable to avoid shadowing
-            $modifiers = $preset->modifiers;
+            $presetModifiers = $preset->modifiers;
 
              foreach ($preset->descriptor->iterateModifiers($modifierCollection) as $mod) {
                  $known[] = $codec->modifiersToPath(array_merge(
-                    $modifiers,
+                    $presetModifiers,
                      $mod,
                  ));
              }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Bridge/Nette/DI/ImageStorageExtension.php` around lines 619 - 636, The
inner variable $modifiers inside the foreach over $presets shadows the outer
$modifiers declared earlier (lines ~584–593); rename the inner variable (e.g.,
to $presetModifiers or $baseModifiers) so it does not overwrite the outer one
and update its use in the array_merge call inside the descriptor loop (the code
that currently does $modifiers = $preset->modifiers and array_merge($modifiers,
$mod) should use the new name), leaving the outer $modifiers untouched.
tests/Modifier/Codec/PresetCodecTest.phpt (1)

23-180: Consider extracting repeated mock setup into a helper or setUp.

Every test method repeats the same four-mock + constructor boilerplate. A setUp() method (or a private factory) would reduce duplication and make the individual tests focus on the scenario-specific expectations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Modifier/Codec/PresetCodecTest.phpt` around lines 23 - 180, Extract the
repeated mock and constructor boilerplate by adding a setUp() in the test class
that creates and assigns properties for the common mocks and SUT (e.g.
$this->innerCodec, $this->config, $this->modifierCollection,
$this->presetCollection and $this->presetCodec = new PresetCodec(...)); update
each test to reuse those properties and only set scenario-specific expectations
(e.g. calls to $this->config->shouldReceive(...),
$this->presetCollection->shouldReceive(...),
$this->innerCodec->shouldReceive(...)); alternatively implement a private
factory method (e.g. createPresetCodec()) that returns the configured mocks and
PresetCodec instance and have tests call that to remove duplication.
src/Modifier/Codec/RuntimeCachedCodec.php (1)

32-37: Cache key for modifiersToPath is order-sensitive on array inputs.

json_encode produces different strings for ['w' => 100, 'h' => 200] vs ['h' => 200, 'w' => 100], leading to cache misses for semantically equivalent modifier sets. If callers consistently produce arrays in the same insertion order this is a non-issue, but worth being aware of.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Modifier/Codec/RuntimeCachedCodec.php` around lines 32 - 37, The cache
key for RuntimeCachedCodec::modifiersToPath is order-sensitive because it
json_encodes arrays; normalize array inputs into a canonical, order-independent
representation before encoding by recursively sorting associative array keys
(leave scalar/string inputs as-is), then json_encode that normalized value for
use as the $key when reading/writing $this->cache['modifiersToPath']; after
normalization call $this->codec->modifiersToPath($value) as before.
ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57d73ed and 9d5a8b3.

📒 Files selected for processing (47)
  • README.md
  • src/Bridge/Nette/DI/Config/PresetConfig.php
  • src/Bridge/Nette/DI/Config/StorageConfig.php
  • src/Bridge/Nette/DI/ImageStorageExtension.php
  • src/Config/Config.php
  • src/FileInfo.php
  • src/FileInfoInterface.php
  • src/ImageServer/LocalImageServer.php
  • src/ImageStorage.php
  • src/LinkGenerator/LinkGenerator.php
  • src/LinkGenerator/LinkGeneratorInterface.php
  • src/Modifier/AbstractModifier.php
  • src/Modifier/Codec/Codec.php
  • src/Modifier/Codec/CodecInterface.php
  • src/Modifier/Codec/PresetCodec.php
  • src/Modifier/Codec/RuntimeCachedCodec.php
  • src/Modifier/Codec/Value/PresetValue.php
  • src/Modifier/Codec/Value/Value.php
  • src/Modifier/Codec/Value/ValueInterface.php
  • src/Modifier/Facade/ModifierFacade.php
  • src/Modifier/Facade/ModifierFacadeFactory.php
  • src/Modifier/Facade/ModifierFacadeInterface.php
  • src/Modifier/Preset/Preset.php
  • src/Modifier/Preset/PresetCollection.php
  • src/Modifier/Preset/PresetCollectionInterface.php
  • src/PathInfo.php
  • src/PathInfoInterface.php
  • src/Responsive/Descriptor/ArgsFacade.php
  • src/Responsive/Descriptor/DescriptorInterface.php
  • src/Responsive/Descriptor/WDescriptor.php
  • src/Responsive/Descriptor/XDescriptor.php
  • src/Responsive/SrcSetGenerator.php
  • src/Security/KnownModifiers.php
  • src/Security/SignatureStrategy.php
  • src/Security/SignatureStrategyInterface.php
  • tests/Bridge/Nette/DI/ImageStorageExtensionTest.php
  • tests/Bridge/Nette/DI/config/ImageStorage/config.withModifiersAndApplicatorsAndValidatorsAndPresets.neon
  • tests/ImageServer/LocalImageServerTest.phpt
  • tests/LinkGenerator/LinkGeneratorTest.phpt
  • tests/Modifier/Codec/CodecTest.phpt
  • tests/Modifier/Codec/PresetCodecTest.phpt
  • tests/Modifier/Codec/RuntimeCachedCodecTest.phpt
  • tests/Modifier/Facade/ModifierFacadeTest.phpt
  • tests/Modifier/Preset/PresetCollectionTest.phpt
  • tests/PathInfoTest.phpt
  • tests/Responsive/Descriptor/ArgsFacadeTest.phpt
  • tests/Security/SignatureStrategyTest.phpt
💤 Files with no reviewable changes (3)
  • src/Modifier/Codec/Value/PresetValue.php
  • src/Modifier/Codec/Value/Value.php
  • src/Modifier/Codec/Value/ValueInterface.php
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Bridge/Nette/DI/ImageStorageExtension.php`:
- Around line 584-593: The assertion in the array_map closure that processes
$extConfig->modifiers is too strict: it checks is_subclass_of($entity,
Modifier\AbstractModifier::class) even though the closure returns
Modifier\ModifierInterface and callers may provide classes (e.g. TestModifier)
that implement Modifier\ModifierInterface without extending AbstractModifier;
update the assertion to check is_subclass_of($entity,
Modifier\ModifierInterface::class) or use instanceof/implements semantics so the
closure in this anonymous function (the Statement $modifier handler that builds
new $entity(...$params)) accepts any class implementing
Modifier\ModifierInterface instead of requiring AbstractModifier.

In `@src/ImageServer/LocalImageServer.php`:
- Line 151: The current assignment to $token casts the result of
$request->getQueryParameter($signatureParameterName) which can be
array|string|null, causing a PHP Notice and PHPStan error when an array is
returned; update the code that sets $token so it first checks the return type
from $request->getQueryParameter($signatureParameterName) (e.g., $value =
$request->getQueryParameter($signatureParameterName)), then if $value is an
array extract the intended element (for example use reset($value) or implode if
multiple are expected), if it's a string use it directly, and if null fall back
to '' — then cast or type-hint $token to string; reference
$request->getQueryParameter, $signatureParameterName and $token to locate and
change the assignment.

In `@src/Modifier/Codec/Codec.php`:
- Around line 28-34: The InvalidArgumentException thrown in modifiersToPath
incorrectly says "decode" which is misleading; update the exception message in
the modifiersToPath method to reflect encoding (e.g., state that a string was
provided but an array of modifiers is required) so it clearly describes the
expected type and that this is an encoding operation; modify the
InvalidArgumentException message text in the modifiersToPath function
accordingly.

In `@src/Modifier/Facade/ModifierFacade.php`:
- Line 17: Remove the dead import "use function is_array;" from
ModifierFacade.php: locate the ModifierFacade class (and the top-of-file
imports) and delete the unused import line; also run a quick grep for any
remaining is_array usage in that class or related methods (e.g., where the
switch changed to is_string($modifiers)) to ensure no other code relies on the
removed import before committing.

In `@src/Responsive/Descriptor/DescriptorInterface.php`:
- Around line 25-34: The docblock for expandModifier contains a duplicate
`@return` and is missing the `@param` for $value; update the PHPDoc above the method
expandModifier(ModifierCollectionInterface $modifierCollection, mixed $value):
array to replace the extra `@return` with a proper `@param` annotation for $value
(e.g., `@param` mixed $value) and keep the single `@return` array<string,
string|numeric|bool>, preserving the `@throws` InvalidArgumentException entry.
- Around line 14-23: The docblock for validateModifierValue documents only the
$value parameter but the method signature includes a second parameter $default;
update the PHPDoc for DescriptorInterface::validateModifierValue to add a `@param`
entry for $default (use the same type union used for $value, e.g. `@param`
string|numeric|bool $default) and ensure the `@return/`@throws lines remain
correct so the docblock matches the method signature.

In `@src/Responsive/Descriptor/WDescriptor.php`:
- Around line 62-85: WDescriptor currently assumes $this->widths has at least
one element and validateModifierValue accesses $this->widths[0], causing an
undefined-offset when constructed empty; fix by adding a guard in the
WDescriptor constructor to validate that the provided widths array is non-empty
(or throw InvalidArgumentException) so instances always have at least one
preset, and keep validateModifierValue as-is (or add a defensive check at the
start of validateModifierValue to throw if $this->widths is empty). Update the
constructor (WDescriptor::__construct) to validate and error early, referencing
the widths property and validateModifierValue method.

In `@tests/Modifier/Codec/CodecTest.phpt`:
- Around line 31-35: Update the error message thrown by the modifiersToPath
function so it uses "encode" (or "convert") instead of "decode": locate the
modifiersToPath implementation and change the InvalidArgumentException message
from "Can not decode value of type string, the value must be array<string,
string|numeric|bool>." to something like "Can not encode value of type string,
the value must be array<string, string|numeric|bool>." so the wording correctly
reflects that modifiersToPath encodes an array to a path.

---

Outside diff comments:
In `@tests/ImageServer/LocalImageServerTest.phpt`:
- Line 366: Rename the test method
testImageResponseShouldBeReturnedIfOnlSourceFileExists to
testImageResponseShouldBeReturnedIfOnlySourceFileExists to fix the typo; update
the method declaration and any references (e.g., in test suites or annotations)
that call or reference testImageResponseShouldBeReturnedIfOnlSourceFileExists so
they point to the corrected method name.

In `@tests/Modifier/Facade/ModifierFacadeTest.phpt`:
- Line 16: Remove the unused import PresetValue from the top of the test (it is
no longer referenced in ModifierFacadeTest after replacing decode(PresetValue)
with expandModifiers); locate the use statement "use
SixtyEightPublishers\ImageStorage\Modifier\Codec\Value\PresetValue;" in
ModifierFacadeTest and delete that line so PHP CS Fixer no_unused_imports
violations are resolved.

In `@tests/Responsive/Descriptor/ArgsFacadeTest.phpt`:
- Line 11: Remove the now-unused import PresetValue from the test file (the
import statement referencing
SixtyEightPublishers\ImageStorage\Modifier\Codec\Value\PresetValue) since it’s
no longer referenced after migrating to expandModifiers; locate the import in
ArgsFacadeTest (tests/Responsive/Descriptor/ArgsFacadeTest.phpt) and delete that
use line (or consolidate imports) so the no_unused_imports CI violation is
resolved.

---

Nitpick comments:
In `@README.md`:
- Around line 90-99: The README example has inconsistent indentation for the
presets: align the nested modifiers children consistently between my_preset and
my_preset_2 (e.g., make the w and ar under my_preset use the same 4-space extra
indentation style as my_preset_2); update the modifiers block under my_preset so
the w and ar lines match the indentation of my_preset_2 (presets, my_preset,
my_preset_2, modifiers, w, ar, defaultW).

In `@src/Bridge/Nette/DI/ImageStorageExtension.php`:
- Around line 619-636: The inner variable $modifiers inside the foreach over
$presets shadows the outer $modifiers declared earlier (lines ~584–593); rename
the inner variable (e.g., to $presetModifiers or $baseModifiers) so it does not
overwrite the outer one and update its use in the array_merge call inside the
descriptor loop (the code that currently does $modifiers = $preset->modifiers
and array_merge($modifiers, $mod) should use the new name), leaving the outer
$modifiers untouched.

In `@src/LinkGenerator/LinkGenerator.php`:
- Around line 49-51: The code calls $pathInfo->getModifiers() twice; cache its
result in a local variable (e.g., $modifiers = $pathInfo->getModifiers()) and
use that variable for the null/empty check and the subsequent logic so you only
invoke getModifiers() once; if $modifiers is null or an empty array call
$pathInfo = $pathInfo->withModifiers(['original' => true]) as before.
- Around line 101-124: The assigner-fallback logic duplicated in
LinkGenerator::resolveDescriptor (using $this->config[Config::MODIFIER_ASSIGNER]
with an empty-string fallback to ':') should be extracted into a single utility
method (e.g., Config::getModifierAssigner() or a small helper like
ModifierAssigner::resolve()) and then used from both
LinkGenerator::resolveDescriptor and PresetCodec::doExpand; update
resolveDescriptor to call the new method instead of repeating lines 107-108 and
remove the duplicate logic from PresetCodec so both locations rely on the shared
implementation.

In `@src/Modifier/Codec/Codec.php`:
- Around line 40-47: The default assigner/separator fallback logic is duplicated
in modifiersToPath and pathToModifiers; extract a private helper method (e.g.,
private function normalizeModifierDelimiters(array $config): array) that reads
$this->config[Config::MODIFIER_ASSIGNER] and Config::MODIFIER_SEPARATOR, applies
the empty() ? ':' / ',' defaults, asserts string types, and returns
['assigner'=>..., 'separator'=>...]; then replace the duplicated blocks in
modifiersToPath and pathToModifiers to call this helper and use the returned
assigner and separator variables.

In `@src/Modifier/Codec/PresetCodec.php`:
- Around line 62-63: The code uses $modifiers[] = $preset->modifiers without
prior declaration; explicitly initialize $modifiers as an empty array before its
first use (e.g., add $modifiers = [] before the loop or before the line that
calls $this->presetCollection->get) so the variable is declared clearly; update
the function in PresetCodec (where $presetAlias is retrieved via
$this->presetCollection->get) to declare $modifiers = [] prior to appending.

In `@src/Modifier/Codec/RuntimeCachedCodec.php`:
- Around line 32-37: The cache key for RuntimeCachedCodec::modifiersToPath is
order-sensitive because it json_encodes arrays; normalize array inputs into a
canonical, order-independent representation before encoding by recursively
sorting associative array keys (leave scalar/string inputs as-is), then
json_encode that normalized value for use as the $key when reading/writing
$this->cache['modifiersToPath']; after normalization call
$this->codec->modifiersToPath($value) as before.

In `@src/Responsive/Descriptor/WDescriptor.php`:
- Around line 87-108: expandModifier duplicates the numeric + in_array
validation already implemented in validateModifierValue; remove the duplicated
logic and delegate to that method (or call a new shared private helper) to
ensure single-source validation. Specifically, inside
WDescriptor::expandModifier call $this->validateModifierValue($value, null) (or
the extracted helper) and use its result to build and return the array with the
width alias (obtained via getByName(Width::class)->getAlias()); if validation
fails let validateModifierValue throw the same InvalidArgumentException so
expandModifier no longer repeats the numeric/in_array checks.

In `@src/Security/KnownModifiers.php`:
- Around line 7-19: Rename the generic property $list to a clearer name (e.g.,
$modifierPaths) in the KnownModifiers class: update the constructor parameter
and promoted property, the public readonly property declaration, and all usages
such as the isKnown(string $modifiers) method to reference the new name (
KnownModifiers::$modifierPaths and its constructor promotion ). Ensure the
docblock stays accurate (array<string, true>) and run tests or static analysis
to catch any remaining references to $list.

In `@tests/Modifier/Codec/PresetCodecTest.phpt`:
- Around line 23-180: Extract the repeated mock and constructor boilerplate by
adding a setUp() in the test class that creates and assigns properties for the
common mocks and SUT (e.g. $this->innerCodec, $this->config,
$this->modifierCollection, $this->presetCollection and $this->presetCodec = new
PresetCodec(...)); update each test to reuse those properties and only set
scenario-specific expectations (e.g. calls to $this->config->shouldReceive(...),
$this->presetCollection->shouldReceive(...),
$this->innerCodec->shouldReceive(...)); alternatively implement a private
factory method (e.g. createPresetCodec()) that returns the configured mocks and
PresetCodec instance and have tests call that to remove duplication.

In `@tests/Modifier/Codec/RuntimeCachedCodecTest.phpt`:
- Around line 47-75: The test method names are out of logical order and unclear:
rename the two methods (currently testStringValueShouldBeDecodedAndCached2 and
testStringValueShouldBeDecodedAndCached) to clearer, purpose-revealing names
such as testPathCanBeDecodedAndResultIsCached for the one asserting
decoding+cache behavior and
testCachedResultIsReturnedWithoutCallingInnerCodecAgain for the one asserting
the inner codec is only called once; update both method declarations (and any
references) accordingly so their names reflect intent and order.

In `@tests/Modifier/Preset/PresetCollectionTest.phpt`:
- Around line 17-36: Add assertions in testPresetsShouldBeAdded to verify that
Preset objects are round-tripped with their descriptor and
defaultDescriptorValue intact: after retrieving each Preset via
PresetCollection::get('a') and ::get('b'), assert the Preset->descriptor equals
the expected descriptor (null in the current diff) and
Preset->defaultDescriptorValue equals the expected default (null in the current
diff) so both properties are explicitly checked along with modifiers; update
expected values if different from null.

Comment thread src/Bridge/Nette/DI/ImageStorageExtension.php
Comment thread src/ImageServer/LocalImageServer.php Outdated
Comment thread src/Modifier/Codec/Codec.php
Comment thread src/Modifier/Facade/ModifierFacade.php Outdated
Comment thread src/Responsive/Descriptor/DescriptorInterface.php
Comment thread src/Responsive/Descriptor/DescriptorInterface.php
Comment thread src/Responsive/Descriptor/WDescriptor.php
Comment thread tests/Modifier/Codec/CodecTest.phpt

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/ImageServer/LocalImageServer.php (1)

151-151: ⚠️ Potential issue | 🟠 Major

# @phpstan-ignore-line`` suppresses the error but does not fix the runtime type hazard.

The array|string|null return type of getQueryParameter is still in play: ?? '' coalesces only null, so an array value (e.g. ?_s[]=foo) passes straight to (string), emitting a Notice: Array to string conversion and producing the literal token "Array". This was flagged in a previous review; the @phpstan-ignore-line annotation silences static analysis but leaves the runtime behaviour unchanged.

🛡️ Proposed fix (from previous review)
-        $token = (string) ($request->getQueryParameter($signatureParameterName) ?? ''); # `@phpstan-ignore-line`
+        $rawToken = $request->getQueryParameter($signatureParameterName);
+        $token = is_string($rawToken) ? $rawToken : '';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ImageServer/LocalImageServer.php` at line 151, The current assignment to
$token in LocalImageServer.php blindly casts the result of
$request->getQueryParameter($signatureParameterName) to string and suppresses
static analysis; instead, handle array returns explicitly: retrieve $param =
$request->getQueryParameter($signatureParameterName), check is_array($param) and
pick a safe scalar (e.g. $param[0] ?? ''), otherwise use the scalar or null
default, then cast to string and remove the `@phpstan-ignore-line`; update the
code around the $token assignment in the LocalImageServer class/method to use
this safe branching.
🧹 Nitpick comments (2)
tests/Modifier/Facade/ModifierFacadeTest.phpt (1)

169-183: Missing test for the new getPresetCollection() accessor.

testModifierCollectionShouldBeReturned and testCodecShouldBeReturned both follow the same trivial pattern — getPresetCollection() is the only new public method on this facade without equivalent coverage.

💡 Suggested test to add
public function testPresetCollectionShouldBeReturned(): void
{
    $presetCollection = Mockery::mock(PresetCollectionInterface::class);
    $facade = $this->createModifierFacade(presetCollection: $presetCollection);

    Assert::same($presetCollection, $facade->getPresetCollection());
}

Would you like me to open a new issue to track this?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Modifier/Facade/ModifierFacadeTest.phpt` around lines 169 - 183, Add a
unit test for the new getPresetCollection() accessor: create a Mockery mock of
PresetCollectionInterface, pass it into createModifierFacade via the
presetCollection parameter, then assert the facade->getPresetCollection()
returns the same mock (similar pattern to testModifierCollectionShouldBeReturned
and testCodecShouldBeReturned). Ensure the test method is named
testPresetCollectionShouldBeReturned and uses Assert::same to compare the
returned value to the provided mock.
src/Modifier/Facade/ModifierFacade.php (1)

104-107: Consider using $this->codec directly for consistency.

Every other private-field access in this class is direct ($this->modifierCollection, $this->config, etc.). Routing through the public getter is inconsistent inside a final class.

♻️ Proposed refactor
-        if (is_string($modifiers)) {
-            $codec = $this->getCodec();
-            $modifiers = $codec->expandModifiers(value: $modifiers);
-        }
+        if (is_string($modifiers)) {
+            $modifiers = $this->codec->expandModifiers($modifiers);
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Modifier/Facade/ModifierFacade.php` around lines 104 - 107, Replace the
indirect getter call with direct private-field access for consistency: inside
the ModifierFacade method containing the snippet (in the final class
ModifierFacade), change uses of $this->getCodec() to $this->codec so the block
becomes $codec = $this->codec; (and keep the subsequent
$codec->expandModifiers(...) logic unchanged); this aligns with other
private-field accesses like $this->modifierCollection and $this->config while
preserving behavior.
ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9d5a8b3 and dcea5ba.

📒 Files selected for processing (6)
  • src/ImageServer/LocalImageServer.php
  • src/Modifier/Facade/ModifierFacade.php
  • tests/Bridge/Nette/DI/ImageStorageExtensionTest.php
  • tests/Modifier/Facade/ModifierFacadeTest.phpt
  • tests/PathInfoTest.phpt
  • tests/Responsive/Descriptor/ArgsFacadeTest.phpt
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/ImageServer/LocalImageServer.php`:
- Line 151: The current assignment to $token in LocalImageServer.php blindly
casts the result of $request->getQueryParameter($signatureParameterName) to
string and suppresses static analysis; instead, handle array returns explicitly:
retrieve $param = $request->getQueryParameter($signatureParameterName), check
is_array($param) and pick a safe scalar (e.g. $param[0] ?? ''), otherwise use
the scalar or null default, then cast to string and remove the
`@phpstan-ignore-line`; update the code around the $token assignment in the
LocalImageServer class/method to use this safe branching.

---

Nitpick comments:
In `@src/Modifier/Facade/ModifierFacade.php`:
- Around line 104-107: Replace the indirect getter call with direct
private-field access for consistency: inside the ModifierFacade method
containing the snippet (in the final class ModifierFacade), change uses of
$this->getCodec() to $this->codec so the block becomes $codec = $this->codec;
(and keep the subsequent $codec->expandModifiers(...) logic unchanged); this
aligns with other private-field accesses like $this->modifierCollection and
$this->config while preserving behavior.

In `@tests/Modifier/Facade/ModifierFacadeTest.phpt`:
- Around line 169-183: Add a unit test for the new getPresetCollection()
accessor: create a Mockery mock of PresetCollectionInterface, pass it into
createModifierFacade via the presetCollection parameter, then assert the
facade->getPresetCollection() returns the same mock (similar pattern to
testModifierCollectionShouldBeReturned and testCodecShouldBeReturned). Ensure
the test method is named testPresetCollectionShouldBeReturned and uses
Assert::same to compare the returned value to the provided mock.

@tg666
tg666 merged commit 6207d52 into master Feb 23, 2026
2 of 8 checks passed
@tg666
tg666 deleted the feature/better-presets branch February 23, 2026 07:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant